{
  # texlive package set
  tl,
  bin,

  lib,
  buildEnv,
  libfaketime,
  makeFontsConf,
  makeWrapper,
  runCommand,
  writeShellScript,
  writeText,
  toTLPkgSets,
  bash,
  perl,

  # common runtime dependencies
  coreutils,
  gawk,
  gnugrep,
  gnused,
  ghostscript,
}:

lib.fix (
  self:
  {
    withDocs ? false,
    withSources ? false,
    requiredTeXPackages ? ps: [ ps.scheme-infraonly ],

    ### texlive.combine backward compatibility
    __extraName ? "combined",
    __extraVersion ? "",
    # emulate the old texlive.combine (e.g. add man pages to main output)
    __combine ? false,
    # adjust behavior further if called from the texlive.combine wrapper
    __fromCombineWrapper ? false,
    # build only the formats of a package (for internal use!)
    __formatsOf ? null,
  }@args:

  let
    ### buildEnv with custom attributes
    buildEnv' =
      args:
      (buildEnv (
        {
          inherit (args) name paths;
        }
        // lib.optionalAttrs (args ? extraOutputsToInstall) { inherit (args) extraOutputsToInstall; }
        // lib.optionalAttrs (args ? pathsToLink) { inherit (args) pathsToLink; }
      )).overrideAttrs
        (
          removeAttrs args [
            "extraOutputsToInstall"
            "pathsToLink"
            "name"
            "paths"
            "pkgs"
          ]
        );

    ### texlive.combine backward compatibility
    # if necessary, convert old style { pkgs = [ ... ]; } packages to attribute sets
    isOldPkgList = p: !p.outputSpecified or false && p ? pkgs && builtins.all (p: p ? tlType) p.pkgs;
    ensurePkgSets =
      ps:
      if !__fromCombineWrapper && builtins.any isOldPkgList ps then
        let
          oldPkgLists = builtins.partition isOldPkgList ps;
        in
        oldPkgLists.wrong ++ lib.concatMap toTLPkgSets oldPkgLists.right
      else
        ps;

    pkgList = rec {
      # resolve dependencies of the packages that affect the runtime
      all =
        let
          packages = ensurePkgSets (requiredTeXPackages tl);
          runtime = builtins.partition (
            p:
            p.outputSpecified or false
            -> builtins.elem (p.tlOutputName or p.outputName) [
              "out"
              "tex"
              "tlpkg"
            ]
          ) packages;
          keySet = p: {
            key =
              p.pname or p.name
              + lib.optionalString (p.outputSpecified or false) ("-" + p.tlOutputName or p.outputName or "");
            inherit p;
            tlDeps = if p ? tlDeps then ensurePkgSets p.tlDeps else (p.requiredTeXPackages or (_: [ ]) tl);
          };
        in
        # texlive.combine: the wrapper already resolves all dependencies
        if __fromCombineWrapper then
          requiredTeXPackages null
        else
          builtins.catAttrs "p" (
            builtins.genericClosure {
              startSet = map keySet runtime.right;
              operator = p: map keySet p.tlDeps;
            }
          )
          ++ runtime.wrong;

      # group the specified outputs
      specified = builtins.partition (p: p.outputSpecified or false) all;
      specifiedOutputs = lib.groupBy (p: p.tlOutputName or p.outputName) specified.right;
      otherOutputNames = builtins.catAttrs "key" (
        builtins.genericClosure {
          startSet = map (key: { inherit key; }) (
            lib.concatLists (builtins.catAttrs "outputs" specified.wrong)
          );
          operator = _: [ ];
        }
      );
      otherOutputs = lib.genAttrs otherOutputNames (n: builtins.catAttrs n specified.wrong);
      outputsToInstall = builtins.catAttrs "key" (
        builtins.genericClosure {
          startSet = map (key: { inherit key; }) (
            [ "out" ]
            ++ lib.optional (otherOutputs ? man) "man"
            ++ lib.concatLists (builtins.catAttrs "outputsToInstall" (builtins.catAttrs "meta" specified.wrong))
          );
          operator = _: [ ];
        }
      );

      # split binary and tlpkg from tex, texdoc, texsource
      bin =
        if __fromCombineWrapper then
          builtins.filter (p: p.tlType == "bin") all # texlive.combine: legacy filter
        else
          otherOutputs.out or [ ] ++ specifiedOutputs.out or [ ];
      tlpkg =
        if __fromCombineWrapper then
          builtins.filter (p: p.tlType == "tlpkg") all # texlive.combine: legacy filter
        else
          otherOutputs.tlpkg or [ ] ++ specifiedOutputs.tlpkg or [ ];

      nonbin =
        if __fromCombineWrapper then
          builtins.filter (p: p.tlType != "bin" && p.tlType != "tlpkg") all # texlive.combine: legacy filter
        else
          (
            if __combine then # texlive.combine: emulate old input ordering to avoid rebuilds
              lib.concatMap (
                p:
                lib.optional (p ? tex) p.tex
                ++ lib.optional ((withDocs || p ? man) && p ? texdoc) p.texdoc
                ++ lib.optional (withSources && p ? texsource) p.texsource
              ) specified.wrong
            else
              otherOutputs.tex or [ ]
              ++ lib.optionals withDocs (otherOutputs.texdoc or [ ])
              ++ lib.optionals withSources (otherOutputs.texsource or [ ])
          )
          ++ specifiedOutputs.tex or [ ]
          ++ specifiedOutputs.texdoc or [ ]
          ++ specifiedOutputs.texsource or [ ];

      # outputs that do not become part of the environment
      nonEnvOutputs = lib.subtractLists [ "out" "tex" "texdoc" "texsource" "tlpkg" ] otherOutputNames;

      # packages that contribute to config files and formats
      fontMaps = lib.filter (p: p ? fontMaps && (p.tlOutputName or p.outputName == "tex")) nonbin;
      sortedFontMaps = builtins.sort (a: b: a.pname < b.pname) fontMaps;
      hyphenPatterns = lib.filter (
        p: p ? hyphenPatterns && (p.tlOutputName or p.outputName == "tex")
      ) nonbin;
      sortedHyphenPatterns = builtins.sort (a: b: a.pname < b.pname) hyphenPatterns;
      formatPkgs = lib.filter (
        p:
        p ? formats
        && (p.outputSpecified or false -> p.tlOutputName or p.outputName == "tex")
        && builtins.any (f: f.enabled or true) p.formats
      ) all;
      sortedFormatPkgs =
        if __formatsOf != null then [ __formatsOf ] else builtins.sort (a: b: a.pname < b.pname) formatPkgs;
      formats = map (
        p:
        self {
          requiredTeXPackages =
            ps:
            [
              ps.scheme-infraonly
              p
            ]
            ++ hyphenPatterns;
          __formatsOf = p;
        }
      ) sortedFormatPkgs;
    };

    # list generated by inspecting `grep -IR '\([^a-zA-Z]\|^\)gs\( \|$\|"\)' "$TEXMFDIST"/scripts`
    # and `grep -IR rungs "$TEXMFDIST"`
    # and ignoring luatex, perl, and shell scripts (those must be patched using postFixup)
    needsGhostscript = lib.any (
      p:
      lib.elem p.pname [
        "context"
        "dvipdfmx"
        "latex-papersize"
        "lyluatex"
      ]
    ) pkgList.bin;

    name =
      if __combine then
        "texlive-${__extraName}-${bin.texliveYear}${__extraVersion}" # texlive.combine: old name name
      else
        "texlive-${bin.texliveYear}-" + (if __formatsOf != null then "${__formatsOf.pname}-fmt" else "env");

    texmfdist = buildEnv' {
      name = "${name}-texmfdist";

      # remove fake derivations (without 'outPath') to avoid undesired build dependencies
      paths = builtins.catAttrs "outPath" pkgList.nonbin;

      # mktexlsr
      nativeBuildInputs = [
        tl.texlive-scripts # for mktexlsr.pl with --sort support
        perl
      ];

      postBuild = # generate ls-R database
        ''
          perl ${tl.texlive-scripts.tex}/scripts/texlive/mktexlsr.pl --sort "$out"
        '';
    };

    tlpkg = buildEnv {
      name = "${name}-tlpkg";

      # remove fake derivations (without 'outPath') to avoid undesired build dependencies
      paths = builtins.catAttrs "outPath" pkgList.tlpkg;
    };

    # the 'non-relocated' packages must live in $TEXMFROOT/texmf-dist
    # and sometimes look into $TEXMFROOT/tlpkg (notably fmtutil, updmap look for perl modules in both)
    texmfroot =
      runCommand "${name}-texmfroot"
        {
          inherit texmfdist tlpkg;
        }
        ''
          mkdir -p "$out"
          ln -s "$texmfdist" "$out"/texmf-dist
          ln -s "$tlpkg" "$out"/tlpkg
        '';

    # texlive.combine: expose info and man pages in usual /share/{info,man} location
    doc = buildEnv {
      name = "${name}-doc";

      paths = [ (texmfdist.outPath + "/doc") ];
      extraPrefix = "/share";

      pathsToLink = [
        "/info"
        "/man"
      ];
    };

    meta = {
      description =
        "TeX Live environment"
        + lib.optionalString withDocs " with documentation"
        + lib.optionalString (withDocs && withSources) " and"
        + lib.optionalString withSources " with sources";
      platforms = lib.platforms.all;
      longDescription =
        "Contains the following packages and their transitive dependencies:\n - "
        + lib.concatMapStringsSep "\n - " (
          p:
          p.pname + (lib.optionalString (p.outputSpecified or false) " (${p.tlOutputName or p.outputName})")
        ) (requiredTeXPackages tl);
    };

    # other outputs
    nonEnvOutputs = lib.genAttrs pkgList.nonEnvOutputs (
      outName:
      buildEnv' {
        inherit name;
        outputs = [ outName ];
        paths = builtins.catAttrs "outPath" (
          pkgList.otherOutputs.${outName} or [ ] ++ pkgList.specifiedOutputs.${outName} or [ ]
        );
        # force the output to be ${outName} or nix-env will not work
        nativeBuildInputs = [
          (writeShellScript "force-output.sh" ''
            export out="''${${outName}-}"
          '')
        ];
        inherit meta passthru;
      }
    );

    passthru = {
      # This is set primarily to help find-tarballs.nix to do its job
      requiredTeXPackages = builtins.filter lib.isDerivation (
        pkgList.bin
        ++ pkgList.nonbin
        ++ lib.optionals (!__fromCombineWrapper) (
          lib.concatMap (
            n: (pkgList.otherOutputs.${n} or [ ] ++ pkgList.specifiedOutputs.${n} or [ ])
          ) pkgList.nonEnvOutputs
        )
      );
      # useful for inclusion in the `fonts.packages` nixos option or for use in devshells
      fonts = "${texmfroot}/texmf-dist/fonts";
      # support variants attrs, (prev: attrs)
      __overrideTeXConfig =
        newArgs:
        let
          appliedArgs = if builtins.isFunction newArgs then newArgs args else newArgs;
        in
        self (args // { __fromCombineWrapper = false; } // appliedArgs);
      withPackages =
        reqs:
        self (
          args
          // {
            requiredTeXPackages = ps: reqs ps ++ requiredTeXPackages ps;
            __fromCombineWrapper = false;
          }
        );
    };

    # TeXLive::TLOBJ::fmtutil_cnf_lines
    fmtutilLine =
      {
        name,
        engine,
        enabled ? true,
        patterns ? [ "-" ],
        options ? "",
        ...
      }:
      lib.optionalString (!enabled) "#! "
      + "${name} ${engine} ${lib.concatStringsSep "," patterns} ${options}";
    fmtutilLines =
      { pname, formats, ... }:
      [
        "#"
        "# from ${pname}:"
      ]
      ++ map fmtutilLine formats;

    # TeXLive::TLOBJ::language_dat_lines
    langDatLine =
      {
        name,
        file,
        synonyms ? [ ],
        ...
      }:
      [ "${name} ${file}" ] ++ map (s: "=" + s) synonyms;
    langDatLines =
      { pname, hyphenPatterns, ... }:
      [ "% from ${pname}:" ] ++ builtins.concatMap langDatLine hyphenPatterns;

    # TeXLive::TLOBJ::language_def_lines
    # see TeXLive::TLUtils::parse_AddHyphen_line for default values
    langDefLine =
      {
        name,
        file,
        lefthyphenmin ? "",
        righthyphenmin ? "",
        synonyms ? [ ],
        ...
      }:
      map (
        n:
        "\\addlanguage{${n}}{${file}}{}{${if lefthyphenmin == "" then "2" else lefthyphenmin}}{${
          if righthyphenmin == "" then "3" else righthyphenmin
        }}"
      ) ([ name ] ++ synonyms);
    langDefLines =
      { pname, hyphenPatterns, ... }:
      [ "% from ${pname}:" ] ++ builtins.concatMap langDefLine hyphenPatterns;

    # TeXLive::TLOBJ::language_lua_lines
    # see TeXLive::TLUtils::parse_AddHyphen_line for default values
    langLuaLine =
      {
        name,
        file,
        lefthyphenmin ? "",
        righthyphenmin ? "",
        synonyms ? [ ],
        ...
      }@args:
      ''
        ''\t['${name}'] = {
        ''\t''\tloader = '${file}',
        ''\t''\tlefthyphenmin = ${if lefthyphenmin == "" then "2" else lefthyphenmin},
        ''\t''\trighthyphenmin = ${if righthyphenmin == "" then "3" else righthyphenmin},
        ''\t''\tsynonyms = { ${lib.concatStringsSep ", " (map (s: "'${s}'") synonyms)} },
      ''
      + lib.optionalString (args ? file_patterns) "\t\tpatterns = '${args.file_patterns}',\n"
      + lib.optionalString (args ? file_exceptions) "\t\thyphenation = '${args.file_exceptions}',\n"
      + lib.optionalString (args ? luaspecial) "\t\tspecial = '${args.luaspecial}',\n"
      + "\t},";
    langLuaLines =
      { pname, hyphenPatterns, ... }: [ "-- from ${pname}:" ] ++ map langLuaLine hyphenPatterns;

    assembleConfigLines = f: packages: builtins.concatStringsSep "\n" (builtins.concatMap f packages);

    updmapLines = { pname, fontMaps, ... }: [ "# from ${pname}:" ] ++ fontMaps;

    out =
      # no indent for git diff purposes
      buildEnv' {

        inherit name;

        # use attrNames, attrValues to ensure the two lists are sorted in the same way
        outputs = [
          "out"
        ]
        ++ lib.optionals (!__combine && __formatsOf == null) (builtins.attrNames nonEnvOutputs);
        otherOutputs = lib.optionals (!__combine && __formatsOf == null) (
          builtins.attrValues nonEnvOutputs
        );

        # remove fake derivations (without 'outPath') to avoid undesired build dependencies
        paths =
          builtins.catAttrs "outPath" pkgList.bin
          ++ lib.optionals (!__combine && __formatsOf == null) pkgList.formats
          ++ lib.optional __combine doc;
        pathsToLink = [
          "/"
          "/share/texmf-var/scripts"
          "/share/texmf-var/tex/generic/config"
          "/share/texmf-var/web2c"
          "/share/texmf-config"
          "/bin" # ensure these are writeable directories
        ];

        nativeBuildInputs = [
          makeWrapper
          libfaketime
          tl."texlive.infra" # mktexlsr
          tl.texlive-scripts # fmtutil, updmap
          tl.texlive-scripts-extra # texlinks
          perl
        ];

        buildInputs = [
          coreutils
          gawk
          gnugrep
          gnused
        ]
        ++ lib.optional needsGhostscript ghostscript;

        inherit meta passthru __combine;
        __formatsOf = __formatsOf.pname or null;

        inherit texmfdist texmfroot;

        fontconfigFile = makeFontsConf { fontDirectories = [ "${texmfroot}/texmf-dist/fonts" ]; };

        fmtutilCnf = assembleConfigLines fmtutilLines pkgList.sortedFormatPkgs;
        updmapCfg = assembleConfigLines updmapLines pkgList.sortedFontMaps;

        languageDat = assembleConfigLines langDatLines pkgList.sortedHyphenPatterns;
        languageDef = assembleConfigLines langDefLines pkgList.sortedHyphenPatterns;
        languageLua = assembleConfigLines langLuaLines pkgList.sortedHyphenPatterns;

        postactionScripts = builtins.catAttrs "postactionScript" pkgList.tlpkg;

        postBuild = ''
          . "${./build-tex-env.sh}"
        '';

        allowSubstitutes = true;
        preferLocalBuild = false;
      };
  in
  # outputsToInstall must be set *after* overrideAttrs (used in buildEnv') or it fails the checkMeta tests
  if __combine || __formatsOf != null then
    out
  else
    lib.addMetaAttrs { inherit (pkgList) outputsToInstall; } out
)
